<?php

declare(strict_types=1);

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Tests\Fixer\LanguageConstruct;

use PhpCsFixer\Tests\Test\AbstractFixerTestCase;

/**
 * @internal
 *
 * @covers \PhpCsFixer\Fixer\LanguageConstruct\NullableTypeDeclarationFixer
 *
 * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\LanguageConstruct\NullableTypeDeclarationFixer>
 *
 * @author John Paul E. Balandan, CPA <paulbalandan@gmail.com>
 *
 * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\LanguageConstruct\NullableTypeDeclarationFixer
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
final class NullableTypeDeclarationFixerTest extends AbstractFixerTestCase
{
    /**
     * @dataProvider provideFixCases
     *
     * @requires PHP 8.0
     */
    public function testFix(string $expected, ?string $input = null): void
    {
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{string, 1?: ?string}>
     */
    public static function provideFixCases(): iterable
    {
        yield 'scalar with null' => [
            "<?php\nfunction foo(?int \$bar): void {}\n",
            "<?php\nfunction foo(int|null \$bar): void {}\n",
        ];

        yield 'class with null' => [
            "<?php\nfunction bar(?\\stdClass \$crate): int {}\n",
            "<?php\nfunction bar(null | \\stdClass \$crate): int {}\n",
        ];

        yield 'static null' => [
            '<?php
class Foo
{
    public function bar(?array $config = null): ?static {}
}
',
            '<?php
class Foo
{
    public function bar(null|array $config = null): null|static {}
}
',
        ];

        yield 'multiple parameters' => [
            "<?php\nfunction baz(?Foo \$foo, int|string \$value, ?array \$config = null): ?int {}\n",
            "<?php\nfunction baz(null|Foo \$foo, int|string \$value, null|array \$config = null): int|null {}\n",
        ];

        yield 'class properties' => [
            '<?php
class Dto
{
    public ?string $name;
    public ?array $parameters;
    public ?int $count;
    public ?Closure $callable;
}
',
            '<?php
class Dto
{
    public null|string $name;
    public array|null $parameters;
    public int|null $count;
    public null|Closure $callable;
}
',
        ];

        yield 'skips more than two atomic types' => [
            "<?php\nstatic fn (int|null|string \$bar): bool => true;\n",
        ];

        yield 'skips already fixed' => [
            "<?php\n\$bar = function (?string \$input): int {};\n",
        ];
    }

    /**
     * @dataProvider provideFix80Cases
     *
     * @requires PHP 8.0
     */
    public function testFix80(string $expected, ?string $input = null): void
    {
        $this->fixer->configure(['syntax' => 'union']);

        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{string, 1?: ?string}>
     */
    public static function provideFix80Cases(): iterable
    {
        yield 'scalar with null' => [
            "<?php\nfunction foo(null|int \$bar): void {}\n",
            "<?php\nfunction foo(?int \$bar): void {}\n",
        ];

        yield 'class with null' => [
            "<?php\nfunction bar(null|\\stdClass \$crate): int {}\n",
            "<?php\nfunction bar(?\\stdClass \$crate): int {}\n",
        ];

        yield 'static null' => [
            '<?php
class Foo
{
    public function bar(null|array $config = null): null|static {}
}
',
            '<?php
class Foo
{
    public function bar(?array $config = null): ?static {}
}
',
        ];

        yield 'multiple parameters' => [
            "<?php\nfunction baz(null|Foo \$foo, int|string \$value, null|array \$config = null): null|int {}\n",
            "<?php\nfunction baz(?Foo \$foo, int|string \$value, ?array \$config = null): ?int {}\n",
        ];

        yield 'class properties' => [
            '<?php
class Dto
{
    public null|\Closure $callable;
    public null|string $name;
    public null|array $parameters;
    public null|int $count;
}
',
            '<?php
class Dto
{
    public ?\Closure $callable;
    public ?string $name;
    public ?array $parameters;
    public ?int $count;
}
',
        ];

        yield 'space after ?' => [
            '<?php
class Foo
{
    private null|int $x;

    public static function from(null|int $x): null|static {}
}
',
            '<?php
class Foo
{
    private ? int $x;

    public static function from(?  int $x): ? static {}
}
',
        ];

        yield 'skips already fixed' => [
            "<?php\n\$bar = function (null | string \$input): int {};\n",
        ];

        yield 'no space before ?' => [
            <<<'PHP'
                <?php
                class Foo
                {
                    public null|int $a;
                    protected null|array $b;
                    private null|string $c;
                    private static null|bool $d;
                }
                PHP,
            <<<'PHP'
                <?php
                class Foo
                {
                    public?int $a;
                    protected?array $b;
                    private?string $c;
                    private static?bool $d;
                }
                PHP,
        ];

        yield 'multiline function declaration' => [
            <<<'PHP'
                <?php function foo(
                    null|string $bar,
                    string $baz
                ) {}
                PHP,
            <<<'PHP'
                <?php function foo(
                    ?string $bar,
                    string $baz
                ) {}
                PHP,
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $config
     *
     * @dataProvider provideFix81Cases
     *
     * @requires PHP 8.1
     */
    public function testFix81(string $expected, ?string $input = null, array $config = []): void
    {
        $this->fixer->configure($config);

        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{string, 1?: ?string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFix81Cases(): iterable
    {
        yield 'readonly property' => [
            '<?php
class Qux
{
    public readonly ?int $baz;
}
',
            '<?php
class Qux
{
    public readonly int|null $baz;
}
',
        ];

        yield 'readonly property with union syntax expected' => [
            '<?php
class Qux
{
    public readonly null|int $baz;
}
',
            '<?php
class Qux
{
    public readonly ?int $baz;
}
',
            ['syntax' => 'union'],
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $config
     *
     * @dataProvider provideFix82Cases
     *
     * @requires PHP 8.2
     */
    public function testFix82(string $expected, ?string $input = null, array $config = []): void
    {
        $this->fixer->configure($config);

        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{string, 1?: ?string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFix82Cases(): iterable
    {
        yield 'skips DNF types' => [
            '<?php
class Infinite
{
    private static (A&B)|null $dft;
}
',
        ];

        yield 'standalone null' => [
            '<?php
class Foo
{
    public function bar(null|array $config = null): null {}
    public function baz(null|ARRAY $config = NULL): NULL {}
}
',
            '<?php
class Foo
{
    public function bar(?array $config = null): null {}
    public function baz(?ARRAY $config = NULL): NULL {}
}
',
            ['syntax' => 'union'],
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFix84Cases
     *
     * @requires PHP 8.4
     */
    public function testFix84(string $expected, ?string $input = null, array $configuration = []): void
    {
        $this->fixer->configure($configuration);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{string, 1?: ?string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFix84Cases(): iterable
    {
        $asymmetricVisibilityWithQuestionMarks = <<<'PHP'
            <?php class Foo
            {
                public public(set) ?int $a;
                public protected(set) ?int $b;
                public private(set) ?int $c;
                public(set) ?int $d;
                protected(set) ?int $e;
                private(set) ?int $f;
            }
            PHP;

        $asymmetricVisibilityWithNullUnionType = <<<'PHP'
            <?php class Foo
            {
                public public(set) null|int $a;
                public protected(set) null|int $b;
                public private(set) null|int $c;
                public(set) null|int $d;
                protected(set) null|int $e;
                private(set) null|int $f;
            }
            PHP;

        yield 'asymmetric visibility - fix `null|T` to `?T`' => [
            $asymmetricVisibilityWithQuestionMarks,
            $asymmetricVisibilityWithNullUnionType,
        ];

        yield 'asymmetric visibility - fix `T|null` to `?T`' => [
            $asymmetricVisibilityWithQuestionMarks,
            str_replace('null|int', 'int|null', $asymmetricVisibilityWithNullUnionType),
        ];

        yield 'asymmetric visibility - fix `?T` to `null|T`' => [
            $asymmetricVisibilityWithNullUnionType,
            $asymmetricVisibilityWithQuestionMarks,
            ['syntax' => 'union'],
        ];

        $abstractAndFinalPropertiesWithQuestionMarks = <<<'PHP'
            <?php abstract class Foo
            {
                abstract public ?int $abstractPublic { set; }
                public abstract ?int $publicAbstract { set; }
                final protected ?int $finalProtected { set(?int $x) { $this->finalProtected = $x; } }
                protected final ?int $protectedFinal { set(?int $i) { $this->protectedFinal = $x; } }
            }
            PHP;

        $abstractAndFinalPropertiesWithNullUnionType = <<<'PHP'
            <?php abstract class Foo
            {
                abstract public null|int $abstractPublic { set; }
                public abstract null|int $publicAbstract { set; }
                final protected null|int $finalProtected { set(?int $x) { $this->finalProtected = $x; } }
                protected final null|int $protectedFinal { set(?int $i) { $this->protectedFinal = $x; } }
            }
            PHP;

        yield 'abstract and final properties - fix `null|T` to `?T`' => [
            $abstractAndFinalPropertiesWithQuestionMarks,
            $abstractAndFinalPropertiesWithNullUnionType,
        ];

        yield 'abstract and final properties - fix `T|null` to `?T`' => [
            $abstractAndFinalPropertiesWithQuestionMarks,
            str_replace('null|int', 'int|null', $abstractAndFinalPropertiesWithNullUnionType),
        ];

        yield 'abstract and final properties - fix `?T` to `null|T`' => [
            $abstractAndFinalPropertiesWithNullUnionType,
            $abstractAndFinalPropertiesWithQuestionMarks,
            ['syntax' => 'union'],
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFix85Cases
     *
     * @requires PHP 8.5
     */
    public function testFix85(string $expected, ?string $input = null, array $configuration = []): void
    {
        $this->fixer->configure($configuration);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{string, 1?: ?string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFix85Cases(): iterable
    {
        $finalPromotedPropertiesWithQuestionMarks = <<<'PHP'
            <?php class Foo
            {
                public function __construct(
                    final public    ?int $a,
                    final protected ?int $b,
                    final private   ?int $c,
                    public final    ?int $d,
                    protected final ?int $e,
                    private final   ?int $f,
                ) {}
            }
            PHP;

        $finalPromotedPropertiesWithNullUnionType = <<<'PHP'
            <?php class Foo
            {
                public function __construct(
                    final public    null|int $a,
                    final protected null|int $b,
                    final private   null|int $c,
                    public final    null|int $d,
                    protected final null|int $e,
                    private final   null|int $f,
                ) {}
            }
            PHP;

        yield 'final promoted properties - fix `null|T` to `?T`' => [
            $finalPromotedPropertiesWithQuestionMarks,
            $finalPromotedPropertiesWithNullUnionType,
        ];

        yield 'final promoted properties - fix `T|null` to `?T`' => [
            $finalPromotedPropertiesWithQuestionMarks,
            str_replace('null|int', 'int|null', $finalPromotedPropertiesWithNullUnionType),
        ];

        yield 'final promoted properties - fix `?T` to `null|T`' => [
            $finalPromotedPropertiesWithNullUnionType,
            $finalPromotedPropertiesWithQuestionMarks,
            ['syntax' => 'union'],
        ];
    }
}
